###############################################################################
#                                                                             #
#    L3q - Light, light, lightweight queue                                    #
#    Copyright (C) 2023  Marcus Pedersén marcus.pedersen@slu.se               #
#                                                                             #
#    This program is free software: you can redistribute it and/or modify     #
#    it under the terms of the GNU General Public License as published by     #
#    the Free Software Foundation, either version 3 of the License, or        #
#    (at your option) any later version.                                      #
#                                                                             #
#    This program is distributed in the hope that it will be useful,          #
#    but WITHOUT ANY WARRANTY; without even the implied warranty of           #
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            #
#    GNU General Public License for more details.                             #
#                                                                             #
#    You should have received a copy of the GNU General Public License        #
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.    #
#                                                                             #
###############################################################################



import sys
import os
import pwd
import grp
import configparser
import logging
import inspect
from sqlite3 import Connection, Cursor
from libl3q.daemon import sql, queue


CONF_FILE: str = '/etc/l3q/l3qd.conf'

'''
This module contains common classes
and functions for all L3q daemon programs.
'''


class LogFilter(logging.Filter):
    '''
    Filters the main log, no errors
    or critical will show up in this log.
    '''
    def filter(self, record: logging.LogRecord) -> bool:
        if(record.levelno <= logging.WARNING):
            return True
        else:
            return False



class JobState:
    '''
    This is the different states
    that a job can be in.
    '''
    RUNNING: str = 'Running'
    QUEUED: str = 'Queued'
    DEPEND: str = 'Depend'
    TERMINATED: str = 'Terminated'
    CANCEL: str = 'Cancel'
    CANCELED: str = 'Canceled'
    ERROR: str = 'Error'
    NODE_ERROR: str = 'Node-error'


class JobType:
    '''
    This is the different types
    a job can have.
    '''
    SEQUENCE: str = "Sequence"
    PARALLEL: str = "Parallel"
    

class NodeState:
    '''
    This is the different states
    that a node can be in.
    '''
    REGISTER: str = 'Register'
    ONLINE: str = 'Online'
    SOFT_ONLINE: str = 'Soft Online'
    OFFLINE: str = 'Offline'
    MAINTENANCE: str = 'Maintenance'
    MAINTENANCE_DRAIN: str = 'Maintenance draining'

    

class L3QdConfig:
    '''
    Contains the needed configuration
    for the  l3q daemon.
    On failure program will terminate.

    Variables:
    validate_mode
    validate_uid

    are required permission and owner
    for validate file.
    '''

    validate_mode: int = 0o100600
    validate_user: str = 'l3q'

    def __init__(self, testing: bool=False) -> None:
        '''
        Init will read config file:
        /etc/l3q/l3qd.conf
        If values are missing default
        values will be used.
        Database file will also be checked
        and initialized.
        Logger will be setup.
        If any error occures error will be
        printed and program will terminate
        with non zero exit code.
        '''

        self.set_defaults(testing)
        self.get_config()
        self.init_log()
        self.validate_init_db()


    def set_defaults(self, testing: bool=False) -> None:
        '''
        Sets the default config values
        '''

        self.port = 39911
        self.validate: dict[str, str] = {}
        self.db_conn: Connection
        self.db_cur: Cursor
        self.user_group = 'l3q'
        self.db_backup_directory = '/var/lib/l3q/backup/'
        self.db_backup_hour = 2
        self.db_backup_minute = 0
        self.db_backup_max_count = 5
        self.poll_intervall = 60
        
        if(testing):
            self.db_file = 'test.db'
            self.l3qd_log = 'test.log'
            self.l3qd_err_log = 'test.err'
            self.testing = True
        else:
            self.db_file = '/var/lib/l3q/l3q.db'
            self.l3qd_log = '/var/log/l3q/l3q.log'
            self.l3qd_err_log = '/var/log/l3q/l3q.err'
            self.queue = 'Default'
            self.testing = False


        
    def get_config(self) -> None:
        '''
        Reads config file and populates
        object with values read from file.
        '''

        conf = configparser.ConfigParser()

        if(self.testing):
            conf.read('test.conf')
        else:
            conf.read(CONF_FILE)

        if(not conf.has_section('l3qd')):
            print('Can not read config file:')
            print(CONF_FILE)
            print('Check configuration for l3qd')

        if(conf.has_option('l3qd', 'port')):
            
            if(not conf['l3qd']['port'].isdigit()):
                print('Parse error in config file:', file=sys.stderr)
                print(CONF_FILE, file=sys.stderr)
                print('Parameter port must be an integer', file=sys.stderr)
                sys.exit(1)
            else:
                self.port = int(conf['l3qd']['port'])
                
        if(conf.has_option('l3qd', 'db_file')):
            self.db_file = conf['l3qd']['db_file']

        if(conf.has_option('l3qd', 'l3qd_log')):
            self.l3qd_log = conf['l3qd']['l3qd_log']

        if(conf.has_option('l3qd', 'l3qd_err_log')):
            self.l3qd_err_log = conf['l3qd']['l3qd_err_log']
        if(conf.has_option('l3qd', 'queue')):
            queues = [m[0] for m in inspect.getmembers(queue, inspect.isclass)
                  if m[1].__module__ == 'libl3q.daemon.queue' and
                  issubclass(eval(f"queue.{m[0]}"), queue.Queue) and
                  m[0] != 'Queue']
            
            if(conf['l3qd']['queue'] in queues):
                self.queue = conf['l3qd']['queue']
            else:
                q = conf['l3qd']['queue']
                print('Parse error in config file:', file=sys.stderr)
                print(CONF_FILE, file=sys.stderr)
                print(f'Parameter queue has no queue called: {q}', file=sys.stderr)
                print(f'Available options: {"".join(queues)}')
                sys.exit(9)
        if(conf.has_option('l3qd', 'user_group')):
            self.user_group = conf['l3qd']['user_group']

        if(conf.has_option('l3qd', 'db_backup_directory')):
            self.db_backup_directory = conf['l3qd']['db_backup_directory']

        if(conf.has_option('l3qd', 'db_backup_hour')):
            
            if(not conf['l3qd']['db_backup_hour'].isdigit()):
                print('Parse error in config file:', file=sys.stderr)
                print(CONF_FILE, file=sys.stderr)
                print('Parameter db_backup_hour must be an integer', file=sys.stderr)
                sys.exit(4)
            else:
                self.db_backup_hour = int(conf['l3qd']['db_backup_hour'])
                
        if(conf.has_option('l3qd', 'db_backup_minute')):
            
            if(not conf['l3qd']['db_backup_minute'].isdigit()):
                print('Parse error in config file:', file=sys.stderr)
                print(CONF_FILE, file=sys.stderr)
                print('Parameter db_backup_minute must be an integer', file=sys.stderr)
                sys.exit(5)
            else:
                self.db_backup_minute = int(conf['l3qd']['db_backup_minute'])

        if(conf.has_option('l3qd', 'db_backup_max_count')):
            
            if(not conf['l3qd']['db_backup_max_count'].isdigit()):
                print('Parse error in config file:', file=sys.stderr)
                print(CONF_FILE, file=sys.stderr)
                print('Parameter db_backup_max_count must be an integer', file=sys.stderr)
                sys.exit(6)
            else:
                self.db_backup_max_count = int(conf['l3qd']['db_backup_max_count'])

        if(conf.has_option('l3qd', 'poll_intervall')):

            if(not conf['l3qd']['poll_intervall'].isdigit()):
                print('Parse error in config file:', file=sys.stderr)
                print(CONF_FILE, file=sys.stderr)
                print('Parameter poll_intervall must be an integer', file=sys.stderr)
                sys.exit(6)
            elif(int(conf['l3qd']['poll_intervall']) < 30):
                print('Parse error in config file:', file=sys.stderr)
                print(CONF_FILE, file=sys.stderr)
                print('Parameter poll_intervall must be an integer >= 30', file=sys.stderr)
                sys.exit(7)
            else:
                self.poll_intervall = int(conf['l3qd']['poll_intervall'])

            
    def init_log(self) -> None:
        '''
        Init log and separate warning and lower
        to main log and erros and critical to 
        error log.
        '''

        self.logger = logging.getLogger('l3qd')
        self.logger.setLevel(logging.DEBUG)
        warn_fh = logging.FileHandler(self.l3qd_log)
        warn_fh.setLevel(logging.INFO)
        warn_fh.addFilter(LogFilter())
        err_fh = logging.FileHandler(self.l3qd_err_log)
        err_fh.setLevel(logging.ERROR)
        formatter = logging.Formatter('%(asctime)s %(levelname)-8s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
        warn_fh.setFormatter(formatter)
        err_fh.setFormatter(formatter)
        self.logger.addHandler(warn_fh)
        self.logger.addHandler(err_fh)

            

    def validate_init_db(self) -> None:
        '''
        Checks permissions and owner of
        the database file, terminates
        program on failure.
        Initializes the database and 
        adds database handles to this object.
        Puts validation dict in this object.
        '''

        if(os.path.exists(self.db_file)):
            stat = os.stat(self.db_file)

            if(int(stat.st_size) == 0):
                print('Database file: {}'.format(self.db_file))
                print('Have zero size')

            if(not self.testing):
                if(int(stat.st_mode) != L3QdConfig.validate_mode):
                    print('Database file: {}'.format(self.db_file), file=sys.stderr)
                    print('has wrong file permissions', file=sys.stderr)
                    print('Required mode is: {}'.format(oct(L3QdConfig.validate_mode - 0o100000)[2:]), file=sys.stderr)
                    self.logger.error('Database file: {} has wrong permissions. Required mode is: {}'.format(self.db_file, oct(L3QdConfig.validate_mode - 0o100000)[2:]))
                    sys.exit(4)

                if(pwd.getpwuid(stat.st_uid)[0] != L3QdConfig.validate_user):
                    print('Database file: {}'.format(self.db_file), file=sys.stderr)
                    print('has wrong file owner', file=sys.stderr)
                    print('Required owner is: {}'.format(L3QdConfig.validate_user), file=sys.stderr)
                    self.logger.error('Database file: {} has wrong owner. Required owner is: {}'.format(self.db_file, L3QdConfig.validate_user))
                    sys.exit(5)

                if(grp.getgrgid(stat.st_gid)[0] != L3QdConfig.validate_user):
                    print('Database file: {}'.format(self.db_file), file=sys.stderr)
                    print('has wrong file group', file=sys.stderr)
                    print('Required group is: {}'.format(L3QdConfig.validate_user), file=sys.stderr)
                    self.logger.error('Database file: {} has wrong file group. Required group is: {}'.format(self.db_file, L3QdConfig.validate_user))
                    sys.exit(6)

        try:
            self.db_conn = sql.init_db(self, self.testing)
            self.db_cur = self.db_conn.cursor()
        except Exception as e:
            print('Error initialize and open database: {}'.format(self.db_file), file=sys.stderr)
            self.logger.error('Initialize and open database: {}, error: {}'.format(self.db_file, e))
            exit(9)

            


